import os, random, math

from datetime import datetime

import lib, sql

from xdh_logging import initLog, LogWriter
from catfile import CatFile
from settings import settings
from cache import cache
from track import Track
from tpd import TPD
from playfile import PlayFile

log = initLog("operator")

proc_compiled = {}
clean_compiled = {}
clean_counters = {}

class Operator(object):
	def __init__(self, mainlist):
		self._mainlist = mainlist
		self.stats = []

	@property
	def tracks(self):
		if not hasattr(self, "_tracks"):
			self._tracks = set(self._mainlist.tracks)
		return self._tracks

	@tracks.setter
	def tracks(self, tracks):
		self._tracks = tracks

	@property
	def holiday(self):
		return hasattr(self._mainlist, "holiday")

	@property
	def cat_tracks(self):
		return self._mainlist.cat_tracks

	@property
	def cat(self):
		if self.holiday:
			return self._mainlist.holiday
		else:
			return self._mainlist.cat

	@property
	def weight(self):
		return self._mainlist.weight

	def filterFavorites(self, keepFavorites = True):
		if keepFavorites:
			return set([track for track in self.tracks if track.favorite])
		else:
			return set([track for track in self.tracks if not track.favorite])

	def finish_stats(self):
		if self.holiday:
			table = sql.HolidaySubStatData
		else:
			table = sql.SubStatData

		sql.process(table, "insert", self.stats)

	@classmethod
	def batch(cls, mainlist, favorites = None):
		if favorites == None:
			favorites = [False, True]

		for favorite in favorites:
			files = list(cls.files())
			random.shuffle(files)

			for filename in files:
				start = datetime.today()

				piece = cls(mainlist, filename, favorite)

				piece.step()

				diff = datetime.today() - start

				piece.write_stat("Total Duration", diff.total_seconds())

				piece.finish_stats()

	def load_tracks(self, source, size):
		dest = []
		source = list(source)
		track_count = 0

		for track in source:
			if track_count >= size:
				break
			if PlayFile.track_ok(track):
				dest.append(track)
				track_count += 1
		return dest


	def write_stat(self, step, value):
		if value > 0:
			if self.holiday:
				catname = "holiday"
			else:
				catname = "category"

			data = { 
				'timestamp': datetime.today(),
				catname: self.cat,
				'sub_category': self.sub,
				'favorite': self.favorite,
				'step': step,
				'value': value,
			}

			self.stats.append(data)

	def stats_played_count(self, tracks, asc = True):
		stats = {}
		for filename in tracks:
			track = Track(filename)
			num_played = len(track.played)
			if num_played not in stats:
				stats[num_played] = 0
			stats[num_played] += 1

		times = sorted(stats.keys(), reverse = not asc)

		for count in times:
			self.write_stat("{num} time{plural}".format(num = count, plural = "s" if count != 1 else ""), stats[count])


	def stats_played_days(self, tracks, asc = True):
		stats = {}
		for filename in tracks:
			track = Track(filename)
			days_since_played = datetime.today() - track.played[-1]
			if days_since_played.days not in stats:
				stats[days_since_played.days] = 0
			stats[days_since_played.days] += 1

		days = sorted(stats.keys(), reverse = not asc)

		for count in days:
			self.write_stat("{num} day{plural}".format(num = count, plural = "s" if count != 1 else ""), stats[count])

	def stats_added_days(self, tracks, asc = True):
		stats = {}
		for filename in tracks:
			track = Track(filename)
			days_since_added = datetime.today() - track.added
			if days_since_added.days not in stats:
				stats[days_since_added.days] = 0
			stats[days_since_added.days] += 1

		days = sorted(stats.keys(), reverse = not asc)

		for count in days:
			self.write_stat("{num} day{plural}".format(num = count, plural = "s" if count != 1 else ""), stats[count])

	def process(self, path, exec_line):
		cls = type(self)
		if self.filename not in cls.compiled:
			log.debug("Compiling a new code instance for file: {filename}".format(filename = self.filename))
			with open(os.path.join(settings.path.base, path, self.filename)) as f:
				source = f.read() + exec_line

				code = compile(source, self.filename, 'exec')

				cls.compiled[self.filename] = code

		else:
			code = cls.compiled[self.filename]

		local_vars = locals()
		local_vars["self"] = self

		log.debug("Execution start for code instance for file: {filename}".format(filename = self.filename))
		start = datetime.today()
		exec code in globals(), local_vars
		diff = datetime.today() - start
		self.write_stat("{filename} Duration".format(filename = self.filename), diff.total_seconds())
		log.debug("Execution end for code instance for file: {filename}".format(filename = self.filename))

	def filterPlayed(self, played):
		if played == None:
			return set(self.tracks)
		elif played:
			return set([track for track in self.tracks if len(track.played) != 0])
		else:
			return set([track for track in self.tracks if len(track.played) == 0])

	def randomizeList(self, num_tracks, played, sub, favorite, current):
		if played != None:
			tracks = set([track for track in self.filterPlayed(played) if track.filename not in current])
		else:
			tracks = set([track for track in self.tracks if track.filename not in current])

		if num_tracks > len(tracks):
			num_tracks = len(tracks)

		filenames = list(tracks)

		random.shuffle(filenames)

		chosen = tracks - set(filenames[0:num_tracks])

		return chosen

	def filterPlayCount(self, tracks, count):
		return set([track for track in tracks if len(track.played) == count])


	def getPlayedCount(self, tracks, least):
		if len(tracks) > 0:
			return len(sorted(list(tracks), key = lambda track: len(track.played), reverse = not least)[0].played)
		else:
			return 0


	def favAbbr(self, sub, favorite):
		abbr = getattr(settings.cat.sub, sub).abbr

		if favorite:
			return settings.favorite.abbr % abbr
		else:
			return abbr


	def favName(self, sub, favorite):
		name = getattr(settings.cat.sub, sub).name

		if favorite:
			return settings.favorite.name % name
		else:
			return name


class Processor(Operator):
	compiled = proc_compiled

	def __init__(self, mainlist, filename, favorite):
		super(Processor, self).__init__(mainlist)

		self.filename = filename
		self.favorite = favorite
		self.removed_tracks = set()
		self.normal_tracks = set()
		self.rand_tracks = set()
		self.added_tracks = set()
		self.not_my_tracks = set()
		self.sub = None
		self.played = None
		self.diff = 0

		for key, value in settings.cat.sub.__dict__.iteritems():
			if value.filename == self.filename:
				self.sub = key
				self.played = value.played
				break

		self.max_tracks = lib.maxTracks(TPD.get(), self.cat, self.sub, self.favorite, len(self.tracks), holiday = self.holiday)

		self.orig_tracks = self.cat_tracks.getSub(self.sub, self.favorite)

		self.others = self.cat_tracks.getRest(self.sub, self.favorite)

		self.sub_tracks = (self.orig_tracks & self.tracks) - self.others

		if self.max_tracks < len(self.sub_tracks):
			self.sub_tracks = set(list(self.sub_tracks)[0:self.max_tracks])


	def purge(self):

		self.removed_tracks |= self.orig_tracks - self.sub_tracks

		for track in self.removed_tracks:
			log.debug(u"<{cat}> {sub}: REMOVED TRACK - {track}".format(cat = self.cat, sub = self.favAbbr(self.sub, self.favorite), track = unicode(track)))

		self.diff = self.max_tracks - len(self.sub_tracks)

		if self.diff < 0:
			self.diff = 0

	def rand_fill(self):
		curr_tracks = self.sub_tracks | self.normal_tracks

		if len(curr_tracks) < self.max_tracks:

			num_rand = self.max_tracks - len(curr_tracks)

			used_tracks = self.others | curr_tracks

			if self.played == None:
				self.tracks -= used_tracks
				played_text = ""

			elif self.played:
				self.tracks = self.filterPlayed(False) - used_tracks
				played_text = "played "

			else:
				self.tracks = self.filterPlayed(True) - used_tracks
				played_text = "unplayed "

			if num_rand > len(self.tracks):
				num_rand = len(self.tracks)

			log.debug("<{cat}> {sub}: no more {kind}tracks left, adding {rand} random tracks.".format(
				cat = self.cat,
				sub = self.favAbbr(self.sub, self.favorite),
				kind = played_text,
				rand = num_rand)
			)

			filenames = list(self.tracks)
                        
			random.shuffle(filenames)
                        
			self.rand_tracks |= set(filenames[0:num_rand])

	def finish(self):
		self.sub_tracks |= self.added_tracks

		self.cat_tracks.setSub(self.sub, self.favorite, self.sub_tracks)
                
		if len(self.removed_tracks) > 0 and len(self.added_tracks) != len(self.removed_tracks):
			log.warning("<{cat}> {sub}: {removed} tracks removed, {added} tracks added".format(
				cat = self.cat,
				sub = self.favAbbr(self.sub, self.favorite),
				removed = len(self.removed_tracks),
				added = len(self.added_tracks)
			))

	def step(self):
		self.write_stat("Original", len(self.orig_tracks))
		self.write_stat("Max", self.max_tracks)
		self.write_stat("Unplayed", len(self.sub_tracks))

		self.purge()
		self.write_stat("Removed", len(self.removed_tracks))

		if self.diff == 0:
			return

		if(self.favorite):
			self.tracks = self.filterFavorites(self.favorite)

		self.tracks -= (self.others | self.sub_tracks)

		if self.played != None:
			self.not_my_tracks = self.filterPlayed(not self.played)
			

		self.tracks = self.filterPlayed(self.played)

		if self.diff > len(self.tracks):
			self.diff = len(self.tracks)

		self.process(path = settings.path.sub_processors, exec_line = "\n\nfunc(self, self.diff, self.tracks, self.normal_tracks)".format(filename = self.filename, fav = " [Favorite]" if self.favorite else ""))
		self.write_stat("Normal", len(self.normal_tracks))
		self.tracks |= self.not_my_tracks # Restoring the tracks not processed.

		self.rand_fill()
		self.write_stat("Random", len(self.rand_tracks))

		self.added_tracks |= self.normal_tracks | self.rand_tracks
		self.write_stat("Total", len(self.added_tracks))


		self.finish()


	@classmethod
	def files(cls):
		if not hasattr(cls, "_files"):
			cls._files = [value.filename for key, value in settings.cat.sub.__dict__.iteritems() if value.type == 'processor']
		return cls._files

	@classmethod
	def set_files(cls, files):
		cls._files = files

class Cleaner(Operator):
	compiled = clean_compiled
	counters = clean_counters

	def __init__(self, mainlist, filename, favorite):
		super(Cleaner, self).__init__(mainlist)

		self.filename = filename
		self.favorite = favorite
		self.removed_tracks = set()

		self.sub = None
		for key, value in settings.cat.sub.__dict__.iteritems():
			if value.filename == filename:
				self.sub = key
				break

		if self.holiday:
			self.track_count = TPD.get() * lib.holidayWeight(self.cat)
		else:
			self.track_count = TPD.get() * lib.holidayWeight() * lib.catWeight(self.cat)

		self.tracks = self.filterFavorites(self.favorite)

		self.count = len(self.tracks)

		if self.filename not in Cleaner.counters:
			Cleaner.counters[self.filename] = eval("lambda self, track_count, count, favorite: {func}".format(func = getattr(settings.cat.sub, self.sub).max_count))

		self.max_calc = Cleaner.counters[self.filename]

		self.max_num = self.max_calc(self, self.track_count, self.count, self.favorite)

	@classmethod
	def files(cls):
		if not hasattr(cls, "_files"):
			cls._files = [value.filename for key, value in settings.cat.sub.__dict__.iteritems() if value.type == 'cleaner']
		return cls._files

	@classmethod
	def set_files(cls, files):
		cls._files = files


	def step(self):
		self.process(path = settings.path.sub_cleaners, exec_line = "\n\nfunc(self, self.max_num, self.tracks, self.removed_tracks)")
		self.write_stat("Max", self.max_num)
		self.write_stat("Removed", len(self.removed_tracks))
		self.tracks -= self.removed_tracks
